/*
 * Copyright 2004 Stottler Henke Associates, Inc.
 * All rights reserved.
 */
package com.stottlerhenke.simbionic.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;


/**
 * Takes a Java class and generates a SimBionic class specification
 * for it.  
 * Duplicated and modified from simbionic.util.ClassSpecGenerator so that it does not exit the program
 * Added jar as classpath
 * 
  */
public class ClassSpecFromJarGenerator
{
    private static String _classList = new String();
    private static ArrayList _classes = new ArrayList();
    private static String _specPath;
    private static Class _theClass;
    private static boolean _append = false;
    private static String _jarFile;
    
    //////////////////////////////////////////////////////////////////////////
    //  New methods that do not use String array as parameters
    //
    
    /**
     * generate(classes, outputFileName, false).
     * @see #generate(List, String, boolean)
     */
    public static void generate(List classes, String outputFileName)
    	throws ClassNotFoundException, IOException
    {
    	generate(classes, outputFileName, false);
    }

    /**
     * Generates the class specification for the given list of classes and saves the generated
     * class specification to the specified file.
     * 
     * @param classes	a list of binary names (e.g., java.lang.String) of the classes whose specification will be generated
     * @param outputFileName	the name of the generated class specification file
     * @param append	true to append the newly generated class specification to the end of the file, false to overwrite
     * 
     * @throws ClassNotFoundException	if any class in classes cannot be found
     * @throws IOException	if any error occur when writing out class specification
     */
    public static void generate(List classes, String outputFileName, boolean append)
    	throws ClassNotFoundException, IOException
    {
    	_classes = new ArrayList(classes);
    	_specPath = outputFileName;
    	_append = append;
    	
    	export();
    }
    
    /**
     * generate(jarURL, classes, outputFileName, false).
     * @see #generate(String, List, String, boolean)
     */
    public static void generate(String jarURL, List classes, String outputFileName)
    	throws ClassNotFoundException, MalformedURLException, IOException, Exception
    {
    	generate(jarURL, classes, outputFileName, false);
    }
    
    /**
     * Generates the class specification for the given list of classes and saves the generated
     * class specification to the specified file.	The given jarURL is going to be added to
     * the classpath in order to find certain or all classes.
     * 
     * @param jarURL	location of the Jar file to be added to the classpath at runtime in order to find classes
     * @param classes	a list of binary names (e.g., java.lang.String) of the classes whose specification will be generated
     * @param outputFileName	the name of the generated class specification file
     * @param append	true to append the newly generated class specification to the end of the file, false to overwrite
     * 
     * @throws ClassNotFoundException	if any class in classes cannot be found
     * @throws MalformedURLException	if the jarURL is malformed
     * @throws IOException	if any error occur when writing out class specification
     * @throws Exception	if the Jar file cannot be added to the classpath at runtime
     */
    public static void generate(String jarURL, List classes, String outputFileName, boolean append)
    	throws ClassNotFoundException, MalformedURLException, IOException, Exception
    {
    	_jarFile = jarURL;
    	_classes = new ArrayList(classes);
    	_specPath = outputFileName;
    	_append = append;
    	
    	// addes the jar file to classpath
    	if (_jarFile != null)
    		addJarToClassPath(_jarFile);
    	
    	export();
    }
    
    private static void export() 
    	throws ClassNotFoundException, IOException
    {
        // write the class spec to memory
        Writer writer = writeClassSpec(_classes);
        	
        // write the class spec to file after
        // generating it successfully to avoid
        // corrupting the existing file
        FileWriter fileWriter = new FileWriter(_specPath, _append);
        fileWriter.write(writer.toString());
        fileWriter.close();
    }
    
    private static void addJarToClassPath(String jar) 
    	throws MalformedURLException, Exception
    {
    	URL jarURL = null;
    	
    	try
    	{
    		jarURL = new URL(jar);
    	}
    	catch (MalformedURLException ex)
    	{
    		jarURL = new File(jar).toURI().toURL();
    	}
    	
    	addURL(jarURL);
    }
    
    private static Writer writeClassSpec(List classBinaryNames)
    	throws ClassNotFoundException, IOException
    {
        StringWriter writer = new StringWriter();
        
    	for (Iterator it = classBinaryNames.iterator(); it.hasNext(); )
    	{
    		String classBinaryName = (String) it.next();
    		
    		// skip the inner classes
            if(!classBinaryName.contains("$"))
            {
            	loadClass(classBinaryName);
                writeSpec(writer,classBinaryName);
            }
    	}
    	
    	return writer;
    }

    //
    //////////////////////////////////////////////////////////////////////////
    
    
    private static int readParameters(String[] args)
    {
        if (args.length < 4)
        {
            System.out.println("Options:");
            System.out.println(" -c <class> path to the class to be analyzed");
            System.out.println(" -jar <file> path to the jar file to be used as classpath");
            System.out.println(" -cl <file> file listing the classes to be analyzed");
            System.out.println(" -o <spec>  path for the generated specification file");
            System.out.println(" -a			appends the specification to the specified file");
            //System.exit(-1);
            return -1;
        }
   
        int arg = 0;
        while (arg < args.length)
        {
            if (args[arg].equals("-c"))
            {
                _classes.add(new String(args[++arg]));
            }
            else if (args[arg].equals("-cl"))
            {
                _classList = args[++arg];
            }
            else if (args[arg].equals("-o"))
            {
                _specPath = args[++arg];
            }
            else if (args[arg].equals("-a"))
            {
                _append = true;
            }
            else if (args[arg].equals("-jar"))
            {
                _jarFile = args[++arg];
            }
            else
            {
                System.out.println("Unknown option '" + args[arg] + "'");
            }
            ++arg;
        }
        return 0;
    }
    
    /**
     * return the status
     * @return exit status: 0: no error, -1: has error
     */
    private static String loadClassList()
    {
        try 
        {
            FileReader reader = new FileReader(_classList);
            BufferedReader in = new BufferedReader(new FileReader(_classList));
            String className;
            while ((className = in.readLine()) != null) {
                className = className.trim();
                _classes.add(new String(className));
            }
            in.close();                        
            reader.close();
        }
        catch (IOException exc)
        {
            System.out.println("Error reading class list.");
            return "Error reading class list.";
            
        }
        return null;
    }
    
    private static void loadClass(String className) throws ClassNotFoundException
    {
        ClassLoader loader = ClassLoader.getSystemClassLoader();
       	System.out.print("Attempting to load class " + className + "...");
       	_theClass = loader.loadClass(className);
       	System.out.println("loaded.");
    }
    
    private static void writeSpec(Writer writer,String className) throws IOException
    {
        writer.write("class " + className + "\r\n");
        
        writeFields(writer);
        writeConstructors(writer);
        writeMethods(writer);
         
        writer.write("endclass\r\n");
    }
    
    private static void writeFields(Writer writer) throws IOException
    {
        // write variables
        Field[] fields = _theClass.getFields();
        for (int i=0; i<fields.length; ++i)
        {
            Field field = fields[i];
            
            // only write out public non-final fields
            int modifiers = field.getModifiers();
            if (!Modifier.isPublic(modifiers) || Modifier.isFinal(modifiers))
                continue;
            
            String name = field.getName();
            
            String type = field.getType().getName();
            
            // no fields with array types
            if (type.indexOf('[') != -1)
                continue;
            
            String isStatic = (Modifier.isStatic(modifiers) ? "static " : "");
            
            writer.write("\tvar " + isStatic + type + " " + name + "\r\n");
        }
    }
    
    private static void writeConstructors(Writer writer) throws IOException
    {
        // write constructors
    	Constructor[] constructors = null;
    	try {
    		constructors = _theClass.getConstructors();
    	} catch (Exception e){
    		System.out.println("error "+e.getMessage());
    	}
        for (int j=0; j<constructors.length; ++j)
        {
            String methodSpec = new String();
            
            Constructor method = constructors[j];
            
            // only write out public methods
            int modifiers = method.getModifiers();
            if (!Modifier.isPublic(modifiers))
                continue;
            
            // the name is fully qualified, so strip off the packages
            String name = method.getName();
            String shortName = name.substring(name.lastIndexOf('.')+1);

            // treat constructors as methods with return type of the class --
            // the engine can distinguish them by their names
            methodSpec += "\tmethod " + name + " " + shortName + "(";
            
            // write params
            boolean badParam = false;
            Class[] paramTypes = method.getParameterTypes();
            for (int p=0; p < paramTypes.length; ++p)
            {
                if (p != 0)
                    methodSpec += ",";
                
                String paramType = paramTypes[p].getName();
                if (paramType.indexOf('[') != -1)
                {
                    badParam = true;
                    break;	// no array param types
                }
                
                methodSpec += paramType + " p" + p;
            }
            
            if (badParam)
                continue;
            
            methodSpec += ")\r\n";
            writer.write(methodSpec);
        }
    }
    
    private static void writeMethods(Writer writer) throws IOException
    {
        // write methods
        Method[] methods = _theClass.getMethods();
        for (int j=0; j<methods.length; ++j)
        {
            String methodSpec = new String();
            
            Method method = methods[j];
            
            // only write out public methods
            int modifiers = method.getModifiers();
            if (!Modifier.isPublic(modifiers))
                continue;
            
            String name = method.getName();

            Class returnType = method.getReturnType();
            String retType = returnType.getName() + " ";
            if (retType.indexOf('[') != -1)
                continue; // no array return types!

            String isStatic = (Modifier.isStatic(modifiers) ? "static " : "");
            
            methodSpec += "\tmethod " + isStatic + retType + name + "(";
            
            // write params
            boolean badParam = false;
            Class[] paramTypes = method.getParameterTypes();
            for (int p=0; p < paramTypes.length; ++p)
            {
                if (p != 0)
                    methodSpec += ",";
                
                String paramType = paramTypes[p].getName();
                if (paramType.indexOf('[') != -1)
                {
                    badParam = true;
                    break;	// no array param types
                }
                
                methodSpec += paramType + " p" + p;
            }
            
            if (badParam)
                continue;
            
            methodSpec += ")\r\n";
            writer.write(methodSpec);
        }
    }

    private static void writeErrorLog(String message)
    {
        FileWriter fileWriter = null;
        try
        {
            File f = new File(_specPath);
            String path = f.getParent();
            fileWriter = new FileWriter(path+"\\auto_error.txt",false);
            fileWriter.write(message);
            fileWriter.close();
        }
        catch (IOException e)
        {
            System.out.println("Error writing error log.");
        }
    }
    
    /**
     * reset the classes list
     * Called before a new execution
     *
     */
    protected static void initialize(){
    	_classes.clear();
    }
    
    /**
     * 
     * @param args
     * @return error returns null if no error. o.w. error message is returned.
     */
    public static String execute(String[] args)
    {
    	initialize();
        readParameters(args);

        // load the jar in classpath if needed
        if(_jarFile!=null){
        	URL url = convertToURL(_jarFile);
        	if(url==null){
        		return "jar file not found."; 
        	}
        	try {
        		addURL(url);
        	} catch (Exception e){
        		return e.getMessage();
        	}
        }
        
        if (!_classList.equals("")){
            String errorMessage = loadClassList();
            if(errorMessage!=null){
            	return errorMessage;
            }
        }
        
        // generate the class spec for all classes
        StringWriter writer = new StringWriter();
        try
        {
	        Iterator classIt = _classes.iterator();
	        while (classIt.hasNext())
	        {
	            String classToLoad = (String)classIt.next();
	            if(classToLoad.contains("$")){
	            	continue;
	            }
	            try
	            {
	            	loadClass(classToLoad);
	            } catch (Exception e){
	            	System.out.println("\nError: " + e.getMessage());
	            	continue;
	            }
	            writeSpec(writer,classToLoad);
	        }            
        }
        catch (Exception e)
        {
            writeErrorLog(e.getMessage());
            System.out.println("\nError generating class spec: " + e.getMessage());
            return "\nError generating class spec: " + e.getMessage();
        }
        	
        // write the spec out
        FileWriter fileWriter = null;
        try
        {
            fileWriter = new FileWriter(_specPath,_append);
            fileWriter.write(writer.toString());
	        fileWriter.close();
        }
        catch (IOException e)
        {
            System.out.println("Error writing class spec file: " + e.getMessage());
            return "Error writing class spec file: " + e.getMessage();
            
        }
        return null;
    }
    
	/** The following code is copied from:
	 * http://forum.java.sun.com/thread.jspa?threadID=300557&start=0&tstart=0
	 * origianlly written by antony_miguel 
	 */
    private static void addURL(URL url) throws Exception{
	       URLClassLoader sysloader = (URLClassLoader)ClassLoader.getSystemClassLoader();
	       try {
	           Method method = URLClassLoader.class.getDeclaredMethod("addURL", new Class[]{URL.class});
	           method.setAccessible(true);
	           method.invoke(sysloader, new Object[]{url});
	       } catch (Throwable t) {
	           t.printStackTrace();
	           throw new Exception("Error, could not add URL to system classloader.", t);
	       }
    }  	
    
    /**
     * convert the given file/url to an URL object. null if fails
     * @param urlStr
     * @return URL
     */
    public static URL convertToURL(String urlStr){
    	URL url = null;
    	try {
    		url = new URL(urlStr);
    	} catch (MalformedURLException me) {
    		try {
    			File file = new File(urlStr); 
    			url = file.toURI().toURL();
    		} catch (Exception e) {
    			return null;
    		}
    	}
    	return url;
    }
}
